<?php

class msnHelper {
    var $passport_url           = 'https://login.live.com/RST.srf';
    var $protocol               = 'MSNP15';
    var $buildver               = '8.1.0178';
    var $prod_key               = 'PK}_A_0N_K%O?A9S';
    var $prod_id                = 'PROD0114ES4Z%Q5W';
    var $login_method           = 'SSO';

    var $oim_send_url           = 'https://ows.messenger.msn.com/OimWS/oim.asmx';
    var $oim_send_soap          = 'http://messenger.live.com/ws/2006/09/oim/Store2';
    var $oim_maildata_url       = 'https://rsi.hotmail.com/rsi/rsi.asmx';
    var $oim_maildata_soap      = 'http://www.hotmail.msn.com/ws/2004/09/oim/rsi/GetMetadata';
    var $oim_read_url           = 'https://rsi.hotmail.com/rsi/rsi.asmx';
    var $oim_read_soap          = 'http://www.hotmail.msn.com/ws/2004/09/oim/rsi/GetMessage';
    var $oim_del_url            = 'https://rsi.hotmail.com/rsi/rsi.asmx';
    var $oim_del_soap           = 'http://www.hotmail.msn.com/ws/2004/09/oim/rsi/DeleteMessages';

    var $membership_url         = 'https://contacts.msn.com/abservice/SharingService.asmx';
    var $membership_soap        = 'http://www.msn.com/webservices/AddressBook/FindMembership';

    var $addmember_url          = 'https://contacts.msn.com/abservice/SharingService.asmx';
    var $addmember_soap         = 'http://www.msn.com/webservices/AddressBook/AddMember';

    var $addcontact_url         = 'https://contacts.msn.com/abservice/abservice.asmx';
    var $addcontact_soap        = 'http://www.msn.com/webservices/AddressBook/ABContactAdd';

    var $delmember_url          = 'https://contacts.msn.com/abservice/SharingService.asmx';
    var $delmember_soap         = 'http://www.msn.com/webservices/AddressBook/DeleteMember';    

    var $client_id              = 0x7000800C;

    var $user                   = '';
    var $password               = '';
    var $authed                 = false;

    const STATUS_ONLINE         = 'NLN';
    const STATUS_BUSY           = 'BSY';
    const STATUS_IDLE           = 'IDL';
    const STATUS_RIGHT_BACK     = 'BRB';
    const STATUS_AWAY           = 'AWY';


    var $server_connection      = false;
    var $stream_timeout         = 1;
    var $timeout                = 15;

    var $passport_policy        = '';

    var $oim_ticket             = '';
    var $contact_ticket         = '';
    var $web_ticket             = '';
    var $space_ticket           = '';
    var $storage_ticket         = '';

    var $font_fn = 'Arial';
    var $font_co = '333333';
    var $font_ef = '';

    var $max_msn_message_len = 1664;
    var $max_yahoo_message_len = 518;


    public $connection         = null;

    public $contact_list       = array();
    private $debug              = true;

    public function __construct( $parent ) {
        $this->connection = &$parent->connection;
        $this->debug = &$parent->debug;
    } // end func

/*************************************************************************************
 * Kapcsolat kezelése
 *************************************************************************************/

    //Kijelentkeztetés
    function logoutError( $error ) {
        $this->connection->writeLn('OUT', '');
        $this->connection->close();
        $this->error = $error;
        $this->debug('*** '.$this->error);
        exit;
    } // end func





    function setPSM( $psm )
    {
        // NS: >>> UUX {id} length
        $str = sprintf('<Data><PSM>%s</PSM><CurrentMedia></CurrentMedia><MachineGuid></MachineGuid></Data>', htmlspecialchars($psm));
        $len = strlen($str);
        $this->connection->writeLn('UUX', $len);
        $this->connection->WriteData($str);
    } // end func

    function setNick( $nick )
    {
        $this->connection->writeLn('PRP', sprintf('MFN %s', rawurlencode($nick)));
    } // end func

    function setStatus( $status )
    {
        // NS: >>> CHG {id} {status} {clientid} {msnobj}
        $this->connection->writeLn('CHG', sprintf('%s %s', $status, $this->client_id));
    } // end func

    function updateContactList( $aContactList ) {
        foreach ( $aContactList as $key=>$val ) {
            
        }
    } // end func




    function getMembershipList()
    {
        $ticket = htmlspecialchars($this->contact_ticket);
        $XML = file_get_contents('soap/getMembershipList.xml');
        $XML = str_replace('%%MSN_TICKET%%', $ticket, $XML);

        $header_array = array(
                            'SOAPAction: '.$this->membership_soap,
                            'Content-Type: text/xml; charset=utf-8',
                            'User-Agent: MSN Explorer/9.0 (MSN 8.0; TmstmpExt)'
                        );
        $this->debug('SOAP: *** Require Membership List');
        $this->debug('SOAP: *** URL: '.$this->membership_url);
        $this->debug('SOAP: >>> Sending SOAP data');

        $curl = curl_init();
        curl_setopt($curl, CURLOPT_URL, $this->membership_url);
        curl_setopt($curl, CURLOPT_HTTPHEADER, $header_array);
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1);
        curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);
        curl_setopt($curl, CURLOPT_POST, 1);
        curl_setopt($curl, CURLOPT_POSTFIELDS, $XML);
        $data = curl_exec($curl);
        $http_code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
        curl_close($curl);

        $this->debug('SOAP: <<< Got Result');

        if ($http_code != 200) return array();

        $data = str_replace(array('soap:', 'xsi:'), array('soap_', 'xsi_'), $data);
        $sXML = simplexml_load_string( $data );

        $aContactList = array();
        $services = $sXML->soap_Body->FindMembershipResponse->FindMembershipResult->Services->Service;
        foreach ( $services as $service ) {
            foreach ( $service->Memberships as $memberships ) {
                foreach ( $memberships as $membership ) {
                    $sMemberRole = $membership->MemberRole;
                    if ( !in_array($sMemberRole, array('Allow', 'Reverse', 'Pending')) )
                        continue;

                    foreach ( $membership->Members->Member as $member ) {
                        $email          = strval($member->PassportName);
                        $displayName    = strval($member->DisplayName);
                        $id             = strval($member->MembershipId);
                        $memberType     = strval($member->attributes()->xsi_type);

                        $network = -1;
                        if ( $memberType == 'PassportMember' ) {
                            $network = 1;

                        } elseif ( $memberType == 'EmailMember' ) {
                            $network = 32;

                        } else {
                            continue;
                        }

                        @list($u_name, $u_domain) = @explode('@', $email);
                        if ($u_domain == NULL) continue;
                        $aContactList[$u_domain][$u_name][$network][strval($sMemberRole)] = $id;
                        if ( $displayName ) {
                            $aContactList[$u_domain][$u_name][$network]['displayName'] = $displayName;
                        }
                    }
                    
                }
            }
        }

        return $aContactList;
    }

    function getMessage( $sMessage, $network = 1 ) {
        $msg_header = 'MIME-Version: 1.0'."\r\n".'Content-Type: text/plain; charset=UTF-8'."\r\n".'X-MMS-IM-Format: FN='.$this->font_fn.'; EF='.$this->font_ef.'; CO='.$this->font_co.'; CS=0; PF=22'."\r\n\r\n";

        $msg_header_len = strlen($msg_header);
        if ($network == 1)
            $maxlen = $this->max_msn_message_len - $msg_header_len;
        else
            $maxlen = $this->max_yahoo_message_len - $msg_header_len;

        $aMessage = array();
        $aStr = @explode("\n", $sMessage);
        $cur_len = 0;
        $msg = '';
        $add_crlf = false;
        foreach ($aStr as $str) {
            $str = str_replace("\r", '', $str);
            $len = strlen($str);
            while ($len > $maxlen) {
                if ($cur_len > 0) {
                    // already has header/msg
                    $aMessage[] = $msg_header.$msg;
                    $cur_len = 0;
                    $msg = '';
                    $add_crlf = false;
               }
                $aMessage[] = $msg_header.substr($str, 0, $maxlen);
                $str = substr($str, $maxlen);
                $len = strlen($str);
            }
            if (($cur_len + $len) > $maxlen) {
                $aMessage[] = $msg_header.$msg;
                $cur_len = 0;
                $msg = '';
                $add_crlf = false;
            }
            if ($msg !== '' || $add_crlf) {
                $msg .= "\r\n";
                $cur_len += 2;
            }
            $add_crlf = true;
            $msg .= $str;
            $cur_len += $len;
        }
        if ($cur_len != 0)
            $aMessage[] = $msg_header.$msg;
        return $aMessage;
    }

    function getChallenge($code)
    {
        // http://msnpiki.msnfanatic.com/index.php/MSNP11:Challenges
        // Step 1: The MD5 Hash
        $md5Hash = md5($code.$this->prod_key);
        $aMD5 = @explode("\0", chunk_split($md5Hash, 8, "\0"));
        for ($i = 0; $i < 4; $i++) {
            $aMD5[$i] = implode('', array_reverse(@explode("\0", chunk_split($aMD5[$i], 2, "\0"))));
            $aMD5[$i] = (0 + base_convert($aMD5[$i], 16, 10)) & 0x7FFFFFFF;
        }

        // Step 2: A new string
        $chl_id = $code.$this->prod_id;
        $chl_id .= str_repeat('0', 8 - (strlen($chl_id) % 8));

        $aID = @explode("\0", substr(chunk_split($chl_id, 4, "\0"), 0, -1));
        for ($i = 0; $i < count($aID); $i++) {
            $aID[$i] = implode('', array_reverse(@explode("\0", chunk_split($aID[$i], 1, "\0"))));
            $aID[$i] = 0 + base_convert(bin2hex($aID[$i]), 16, 10);
        }

        // Step 3: The 64 bit key
        $magic_num = 0x0E79A9C1;
        $str7f = 0x7FFFFFFF;
        $high = 0;
        $low = 0;
        for ($i = 0; $i < count($aID); $i += 2) {
            $temp = $aID[$i];
            $temp = bcmod(bcmul($magic_num, $temp), $str7f);
            $temp = bcadd($temp, $high);
            $temp = bcadd(bcmul($aMD5[0], $temp), $aMD5[1]);
            $temp = bcmod($temp, $str7f);

            $high = $aID[$i+1];
            $high = bcmod(bcadd($high, $temp), $str7f);
            $high = bcadd(bcmul($aMD5[2], $high), $aMD5[3]);
            $high = bcmod($high, $str7f);

            $low = bcadd(bcadd($low, $high), $temp);
        }

        $high = bcmod(bcadd($high, $aMD5[1]), $str7f);
        $low = bcmod(bcadd($low, $aMD5[3]), $str7f);

        $new_high = bcmul($high & 0xFF, 0x1000000);
        $new_high = bcadd($new_high, bcmul($high & 0xFF00, 0x100));
        $new_high = bcadd($new_high, bcdiv($high & 0xFF0000, 0x100));
        $new_high = bcadd($new_high, bcdiv($high & 0xFF000000, 0x1000000));
        // we need integer here
        $high = 0+$new_high;

        $new_low = bcmul($low & 0xFF, 0x1000000);
        $new_low = bcadd($new_low, bcmul($low & 0xFF00, 0x100));
        $new_low = bcadd($new_low, bcdiv($low & 0xFF0000, 0x100));
        $new_low = bcadd($new_low, bcdiv($low & 0xFF000000, 0x1000000));
        // we need integer here
        $low = 0+$new_low;

        // we just use 32 bits integer, don't need the key, just high/low
        // $key = bcadd(bcmul($high, 0x100000000), $low);

        // Step 4: Using the key
        $md5Hash = md5($code.$this->prod_key);
        $aHash = @explode("\0", chunk_split($md5Hash, 8, "\0"));

        $hash = '';
        $hash .= sprintf("%08x", (0 + base_convert($aHash[0], 16, 10)) ^ $high);
        $hash .= sprintf("%08x", (0 + base_convert($aHash[1], 16, 10)) ^ $low);
        $hash .= sprintf("%08x", (0 + base_convert($aHash[2], 16, 10)) ^ $high);
        $hash .= sprintf("%08x", (0 + base_convert($aHash[3], 16, 10)) ^ $low);

        return $hash;
    }

    function getPassportTickets($url = '')
    {
        $user = $this->user;
        $password = htmlspecialchars($this->password);

        if ($url === '')
            $passport_url = $this->passport_url;
        else
            $passport_url = $url;

        //Generate XML answer from template
        $XML    = file_get_contents('soap/getTicket.xml');
        $from   = array('%%MSN_USER_NAME%%', '%%MSN_USER_PASSWORD%%', '%%MSN_PASSPORT_POLICY%%');
        $to     = array($user, $password, $this->passport_policy);
        $XML    = str_replace($from, $to, $XML);

        $this->debug('SOAP: *** Getting Passport Tickets');
        $this->debug('SOAP: *** URL: '.$passport_url);
        $this->debug('SOAP: >>> Ticket request');

        $curl = curl_init();
        curl_setopt($curl, CURLOPT_URL, $passport_url);
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1);
        curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);
        curl_setopt($curl, CURLOPT_POST, 1);
        curl_setopt($curl, CURLOPT_POSTFIELDS, $XML);

        $data = curl_exec($curl);

        file_put_contents('log/passportTikcets.xml', $data);

        $http_code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
        curl_close($curl);
        $this->debug('SOAP: <<< Got Result');

        if ($http_code != 200) {
            // sometimes, rediret to another URL
            //<faultcode>psf:Redirect</faultcode>
            //<psf:redirectUrl>https://msnia.login.live.com/pp450/RST.srf</psf:redirectUrl>
            //<faultstring>Authentication Failure</faultstring>
            if (strpos($data, '<faultcode>psf:Redirect</faultcode>') === false) {
                $this->debug('*** Can\'t get passport ticket! http code = '.$http_code);
                return false;
            }
            preg_match("#<psf\:redirectUrl>(.*)</psf\:redirectUrl>#", $data, $matches);
            if (count($matches) == 0) {
                $this->debug('*** redirect, but can\'t get redirect URL!');
                return false;
            }
            $redirect_url = $matches[1];
            if ($redirect_url == $passport_url) {
                $this->debug('*** redirect, but redirect to same URL!');
                return false;
            }
            $this->debug('*** redirect to '.$redirect_url);
            return $this->getPassportTickets($redirect_url);
        }

        // sometimes, rediret to another URL, also return 200
        //<faultcode>psf:Redirect</faultcode>
        //<psf:redirectUrl>https://msnia.login.live.com/pp450/RST.srf</psf:redirectUrl>
        //<faultstring>Authentication Failure</faultstring>
        if (strpos($data, '<faultcode>psf:Redirect</faultcode>') !== false) {
            preg_match("#<psf\:redirectUrl>(.*)</psf\:redirectUrl>#", $data, $matches);
            if (count($matches) != 0) {
                $redirect_url = $matches[1];
                if ($redirect_url == $passport_url) {
                    $this->debug('*** redirect, but redirect to same URL!');
                    return false;
                }
                $this->debug('*** redirect to '.$redirect_url);
                return $this->getPassportTickets($redirect_url);
            }
        }

        // no Redirect faultcode or URL
        // we should get the ticket here

        preg_match("#".
                   "<wsse\:BinarySecurityToken Id=\"Compact1\">(.*)</wsse\:BinarySecurityToken>(.*)".
                   "<wst\:BinarySecret>(.*)</wst\:BinarySecret>(.*)".
                   "<wsse\:BinarySecurityToken Id=\"PPToken2\">(.*)</wsse\:BinarySecurityToken>(.*)".
                   "<wsse\:BinarySecurityToken Id=\"Compact3\">(.*)</wsse\:BinarySecurityToken>(.*)".
                   "<wsse\:BinarySecurityToken Id=\"Compact4\">(.*)</wsse\:BinarySecurityToken>(.*)".
                   "<wsse\:BinarySecurityToken Id=\"Compact5\">(.*)</wsse\:BinarySecurityToken>(.*)".
                   "<wsse\:BinarySecurityToken Id=\"Compact6\">(.*)</wsse\:BinarySecurityToken>(.*)".
                   "#",
                   $data, $matches);

        // no ticket found!
        if (count($matches) == 0) {
            $this->debug('*** Can\'t get passport ticket!');
            return false;
        }

        // yes, we get ticket
        $aTickets = array(
                    'ticket' => html_entity_decode($matches[1]),
                    'secret' => html_entity_decode($matches[3]),
                    'web_ticket' => html_entity_decode($matches[5]),
                    'contact_ticket' => html_entity_decode($matches[7]),
                    'oim_ticket' => html_entity_decode($matches[9]),
                    'space_ticket' => html_entity_decode($matches[11]),
                    'storage_ticket' => html_entity_decode($matches[13])
                    );

        return $aTickets;
    }

    function deriveKey($key, $magic)
    {
        $hash1 = mhash(MHASH_SHA1, $magic, $key);
        $hash2 = mhash(MHASH_SHA1, $hash1.$magic, $key);
        $hash3 = mhash(MHASH_SHA1, $hash1, $key);
        $hash4 = mhash(MHASH_SHA1, $hash3.$magic, $key);
        return $hash2.substr($hash4, 0, 4);
    }

    function generateLoginBLOB($key, $challenge)
    {
        $key1 = base64_decode($key);
        $key2 = $this->deriveKey($key1, 'WS-SecureConversationSESSION KEY HASH');
        $key3 = $this->deriveKey($key1, 'WS-SecureConversationSESSION KEY ENCRYPTION');

        // get hash of challenge using key2
        $hash = mhash(MHASH_SHA1, $challenge, $key2);

        // get 8 bytes random data
        $iv = substr(base64_encode(rand(1000,9999).rand(1000,9999)), 2, 8);

        $cipher = mcrypt_cbc(MCRYPT_3DES, $key3, $challenge."\x08\x08\x08\x08\x08\x08\x08\x08", MCRYPT_ENCRYPT, $iv);

        $blob = pack('LLLLLLL', 28, 1, 0x6603, 0x8004, 8, 20, 72);
        $blob .= $iv;
        $blob .= $hash;
        $blob .= $cipher;

        return base64_encode($blob);
    }


    function debug( $message, $hide = false ) {
        if ( !$this->debug )
            return false;
            
        echo date('[Y-m-d H:i:s]').'[DEBUG] - '.$message."\n";
    } // end func

    function log( $message, $hide = false ) {
        echo date('[Y-m-d H:i:s]').'[LOG] - '.$message."\n";
    } // end func

} // end class

?>